/*
 * Copyright (c) 2012 Sean Connelly
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

//
// generic tree implementation
//

//
// a tree is made of nodes and boxes
//
// nodes are created via: tree.createNode('nodeName', userData)
// boxes are created via: tree.createBox('boxName', userData)
//
// nodes have named children, which point to other nodes or boxes
// boxes have numbered children, which point to other nodes or boxes
//
// use nodes when each element is unique in some way (and is labelled as such via the link name)
// use boxes to group a bunch of elements that are the same
//
// for example, a Statement would be a node, but a StatementList would be a box (containing a list
// of Statement nodes)
//

var common = require('./common.js');

// specifically coded to dump a Rho AST data field
function tree_dumpAstData(data, show_markers)
{
	switch (typeof data)
	{
	case 'boolean':
	case 'number':
	case 'string':
		return data;
	case 'object':
		if (data === null)
			return null;
		else if (common.isArray(data))
		{
			var out = [];
			for (var i in data)
			{
				var res = tree_dumpAstData(data[i]);
				if (typeof data[i] !== 'boolean' && res === false)
					continue;
				out.push(res);
			}
			return out;
		}
		var out = {};
		for (var k in data)
		{
			if (k === 'marker') // special logic for markers to trim them down a bit
			{
				if (show_markers)
				{
					out.marker = {
							start: data.marker.start.pos,
							stop: data.marker.stop.pos
						};
				}
				continue;
			}
			else if (k === 'toStringResult') // ignore this
				continue;
			else if (k === 'val' && typeof data.tokenName !== 'undefined' && data.tokenName === 'Ratio')
			{
				out.val = data.val.dump();
				continue;
			}
			var res = tree_dumpAstData(data[k]);
			if (typeof data[k] !== 'boolean' && res === false)
				continue;
			out[k] = res;
		}
		return out;
	case 'function':
		return false;
//	case 'undefined':
//		return 'undefined';
	}
	throw 'unknown data type in AST data: ' + typeof data;
}

function tree_createNode(nodeName, data)
{
	if (typeof data == 'undefined')
		data = {};
	var root = false;
	var parent = false;
	var child = {};
	var obj = {
			// functions shared between nodes and boxes (implemented differently sometimes, though)
			isRoot: function() {
					return this === root;
				},
			isNode: function() {
					return true;
				},
			isBox: function() {
					return false;
				},
			getName: function() {
					return nodeName;
				},
			setName: function(newName) {
					nodeName = newName;
					return this;
				},
			getData: function() {
					return data;
				},
			setData: function(newData) {
					data = newData;
					return this;
				},
			updateData: function(kv) {
					for (var k in kv)
						data[k] = kv[k];
					return this;
				},
			setRoot: function(newRoot) {
					root = newRoot;
					for (var k in child)
						child[k].setRoot(root);
					return this;
				},
			setParent: function(newParent) {
					parent = newParent;
					return this;
				},
			toRoot: function() {
					return root;
				},
			toParent: function() {
					return parent;
				},
			each: function(func) {
					for (var k in child)
					{
						if (func.apply(child[k], [k]) === false)
							break;
					}
					return this;
				},
			// functions specific to nodes
			hasChild: function(link) {
					return typeof child[link] !== 'undefined';
				},
			toChild: function(link) {
					if (typeof child[link] == 'undefined')
						throw 'unknown child: ' + link;
					return child[link];
				},
			removeChild: function(link) {
					if (typeof child[link] == 'undefined')
						throw 'unknown child: ' + link;
					delete child[link];
					return this;
				},
			setTree: function(link, tree) {
					child[link] = tree.setRoot(root).setParent(this);
					return this;
				},
			setNode: function(link, newName, theData) {
					if (typeof newName == 'undefined')
						newName = link;
					return this.setTree(link, tree_createNode(newName, theData));
				},
			setBox: function(link, newName, theData) {
					if (typeof newName == 'undefined')
						newName = link;
					return this.setTree(link, tree_createBox(newName, theData));
				},
			toSetTree: function(link, tree) {
					return this.setTree(link, tree).toChild(link);
				},
			toSetNode: function(link, newName, theData) {
					return this.setNode(link, newName, theData).toChild(link);
				},
			toSetBox: function(link, newName, theData) {
					return this.setBox(link, newName, theData).toChild(link);
				},
			dump: function() {
					//console.log('-------- dumping tree --------');
					this._dump('', 0);
					return this;
				},
			_dump: function(pfx, tabsize) {
					console.log((new Array(tabsize + 1)).join(' ') + pfx + ': ' + this.getName());//, this.getData());
					if (tabsize < 6)
					{
						this.each(function(idx) {
								this._dump(idx, tabsize + 1);
							});
					}
				},
			dumpAst: function(show_markers) {
					var out = { name: this.getName() };

					var link = {};
					this.each(function(idx) {
							link[idx] = this.dumpAst(show_markers);
						});
					if (!common.isEmptyObject(link))
						out.link = link;

					var data = tree_dumpAstData(this.getData(), show_markers);
					if (!common.isEmptyObject(data))
						out.data = data;

					return out;
				}
		};
	root = obj;
	parent = obj;
	return obj;
};

function tree_createBox(boxName, data)
{
	if (typeof data == 'undefined')
		data = {};
	var root = false;
	var parent = false;
	var list = [];
	var lastIndex = false;
	var obj = {
			// functions shared between nodes and boxes (implemented differently sometimes, though)
			isRoot: function() {
					return this === root;
				},
			isNode: function() {
					return false;
				},
			isBox: function() {
					return true;
				},
			getName: function() {
					return boxName;
				},
			setName: function(newName) {
					boxName = newName;
					return this;
				},
			getData: function() {
					return data;
				},
			setData: function(newData) {
					data = newData;
					return this;
				},
			updateData: function(kv) {
					for (var k in kv)
						data[k] = kv[k];
					return this;
				},
			setRoot: function(newRoot) {
					root = newRoot;
					for (var i = 0; i < list.length; i++)
						list[i].setRoot(root);
					return this;
				},
			setParent: function(newParent) {
					parent = newParent;
					return this;
				},
			toRoot: function() {
					return root;
				},
			toParent: function() {
					return parent;
				},
			each: function(func) {
					for (var i = 0; i < list.length; i++)
					{
						if (func.apply(list[i], [i]) === false)
							break;
					}
					return this;
				},
			// functions specific to boxes
			getLastIndex: function() {
					return lastIndex;
				},
			size: function() {
					return list.length;
				},
			toIndex: function(idx) {
					if (idx < 0 || idx >= list.length)
						throw 'unknown index: ' + idx;
					return list[idx];
				},
			addTree: function(tree) {
					lastIndex = list.length;
					list.push(tree.setRoot(root).setParent(this));
					return this;
				},
			prependTree: function(tree) {
					lastIndex = 0;
					list.unshift(tree.setRoot(root).setParent(this));
					return this;
				},
			addNode: function(newName, theData) {
					return this.addTree(tree_createNode(newName, theData));
				},
			addBox: function(newName, theData) {
					return this.addTree(tree_createBox(newName, theData));
				},
			toAddTree: function(tree) {
					return this.addTree(tree).toIndex(lastIndex);
				},
			toAddNode: function(newName, theData) {
					return this.addNode(newName, theData).toIndex(lastIndex);
				},
			toAddBox: function(newName, theData) {
					return this.addBox(newName, theData).toIndex(lastIndex);
				},
			dump: function() {
					//console.log('-------- dumping tree --------');
					this._dump('', 0);
					return this;
				},
			_dump: function(pfx, tabsize) {
					console.log((new Array(tabsize + 1)).join(' ') + pfx + ': ' + this.getName(), this.getData());
					if (tabsize < 6)
					{
						this.each(function(idx) {
								this._dump(idx, tabsize + 1);
							});
					}
				},
			dumpAst: function(show_markers) {
					var out = { name: this.getName() };

					var list = [];
					this.each(function(idx) {
							list.push(this.dumpAst(show_markers));
						});
					if (list.length > 0)
						out.list = list;

					var data = tree_dumpAstData(this.getData(), show_markers);
					if (!common.isEmptyObject(data))
						out.data = data;

					return out;
				}
		};
	root = obj;
	parent = obj;
	return obj;
}

exports.createNode = tree_createNode;
exports.createBox = tree_createBox;
